Automating Your TypeScript Index Files With Tsndexer
Automating Your TypeScript Index Files with tsndexer
TypeScript projects often use “barrel” files – typically named index.ts (or index.tsx for React components) – to re-export modules from a directory. These barrel files make it easier to import from a single place instead of many deep paths. However, manually maintaining dozens or hundreds of index.ts files is tedious and error-prone, especially in large codebases. In this post, we’ll explore why barrel files are useful, the pain points of managing them by hand, and how the tsndexer Go CLI tool can generate these index files automatically across your project. We’ll also look at tsndexer’s features (like --dry-run, --namespace, --ignore) and compare it to other solutions (such as barrelsby, barrel-cli, and various JavaScript ecosystem plugins).
Our goal is to improve your TypeScript developer experience (DX) by automating away the boring parts of managing exports. Let’s dive in!
Why Use Barrel (Index) Files in TypeScript?
Before tackling automation, it’s worth understanding the motivation for using barrel files in the first place. An index file in a folder acts as a central hub for all exports in that folder. Instead of importing from individual files with long paths, you import from the folder’s index, which re-exports everything inside. This brings several benefits:
- Cleaner Imports: Barrel files make import statements shorter and more readable. You can import from a higher-level directory rather than a deep nested file path. This leads to more maintainable import lines.
- Easy Maintenance: When you add a new module or change a file’s location, you only need to update one index file instead of fixing imports all over your code. All consuming code continues to import from the same place.
- Modular Structure: Barrels help organize the project into logical modules. Each folder’s index clearly enumerates that module’s public exports, keeping the codebase well-organized.
- Consistent Import Paths: Teams can standardize on importing from module barrels, which improves consistency and developer experience. It prevents situations where some files import
../../utils/foowhile others import../../../common/utils/foo– everyone just doesimport { foo } from '.../utils'via the index.
In short, barrel files can greatly simplify imports. For example, without barrels you might see code like:
// Without an index.ts barrel
import { DropDown } from "./src/controls/DropDown";
import { TextBox } from "./src/controls/TextBox";
import { CheckBox } from "./src/controls/CheckBox";
// ...and so on for each component
With an index.ts in src/controls/, you can collapse those to a single line:
// With an index.ts barrel
import { DropDown, TextBox, CheckBox } from "./src/controls";
This is much tidier and easier to read. In a React project, a barrel file might let you do:
import { Button, Input, Checkbox } from "./components";
instead of importing each component file individually. In large projects, this significantly improves code cleanliness and consistency.
However, these benefits come with a cost: someone has to write and update those index files!
The Pain of Maintaining Index Files Manually
Creating and keeping index files up-to-date by hand can become a nightmare as your project grows. Some common developer pain points include:
- Tedious Repetition: Every time you create a new file or folder, you must remember to add an export in the appropriate
index.ts. Doing this for dozens of new files is boring and error-prone. It’s easy to make mistakes or just forget. - Missing Exports & Errors: If you forget to export a module in the index, it won’t be available for import. This often surfaces as a “Module has no exported member” error at compile time. For example, a library might add a new component but forget to export it in
index.ts, leading to consumer code failing to import it properly (and perhaps resorting to a deep import hack). In one real issue, a TypeScript library didn’t exportVariantand some schemes in its index, so importing from the package root caused errors – the fix was to add the missingexport * from ...lines in the index. Missing exports can break shared packages and confuse developers. - Deep Import Paths: Without barrels, developers may import directly from deep file paths. This leads to ugly imports like
import { something } from "../../../../../lib/utils/date";. Such deep paths are hard to manage and refactor. An index file flattens that – e.g.import { formatDate } from "lib/utils"– making code easier to navigate. - Inconsistent or Stale Exports: Manually maintained index files can drift out of sync. One team member might add a new utility but not update the barrel, so others are unaware of it or have to import it from a different path. Or an index might still export a file that was moved or renamed, causing broken imports.
- Poor DX for Consumers: In a shared library, if index exports are missing or incomplete, users of the library get a bad experience. They might try to import something that isn’t exported and hit errors, or be forced to reach into internal module paths (which is generally discouraged and may break on updates). Well-maintained barrels serve as the “public API” of a module or package. If they’re wrong, developers suffer.
In summary, maintaining barrel files by hand doesn’t scale. It’s easy to make mistakes and it’s a chore that detracts from actual feature development. As one developer succinctly put it, “Making index files is quite tedious.”
Wouldn’t it be nice if this could be automated? This is where tsndexer comes in.
Introducing tsndexer – A CLI to Auto-Index Your Files
tsndexer is a command-line tool (written in Go) that automatically generates index.ts/index.tsx barrel files for you. Instead of manually editing each index file whenever your project structure changes, you run tsndexer and it takes care of creating or updating all the index files across your project. The tool scans your directories for TypeScript files and produces index files that export everything in those directories.
Key benefits of using tsndexer:
- No More Manual Updates: The tool ensures every file is exported exactly where it should be. No missing exports, no outdated imports – it eliminates human error in barrel maintenance.
- Save Time in Large Projects: For a project with hundreds of files or deeply nested folders, generating indexes by hand is a huge time sink.
tsndexercan update the entire project’s index files in seconds, freeing you from this repetitive work. - Improved DX & Consistency: With
tsndexer, your exports are always consistent and complete. Developers can confidently import from index files and get everything they expect, which improves the developer experience (no mysterious “export not found” errors at runtime). In shared codebases or monorepos, this consistency is a big win. - Zero Dependencies & Easy Setup: Because
tsndexeris a compiled Go binary, it doesn’t require installing any Node.js packages or adding heavy devDependencies. You can simply download the binary (or build from source) and run it. There’s no need to include a library in your project or even have Node installed – ideal if you want a lightweight tool in CI pipelines or if your project isn’t Node-based. In contrast, many other barrel tools are Node scripts that you must install via npm (likebarrelsbyorcreate-ts-index). - Simple CLI Usage:
tsndexeris designed to be straightforward. Point it at your source directory (for example,src/) and it will generate index files recursively. You don’t need to write a config file for basic use – sensible defaults are built-in. As we’ll see, it also provides a few useful options for flexibility.
In essence, tsndexer automates the barrel file pattern so you can enjoy the benefits of index files without the maintenance headache. Let’s see it in action with a real-world example.
Using tsndexer on a Project (Example)
Imagine we have a TypeScript project with the following structure (before running tsndexer):
src
├── components
│ ├── Button.tsx
│ ├── Input.tsx
│ └── common
│ └── Icon.tsx
└── utils
├── date.ts
└── string.ts
In this project, we have a components folder (with a subfolder common) and a utils folder, but no index files yet. Our imports might look like:
import { Button } from "../src/components/Button";
import { Icon } from "../src/components/common/Icon";
import { formatDate } from "../src/utils/date";
These work, but they’re not ideal – they’re reaching into specific files. We’d prefer to import from components or utils directly. To do that, we need index files in those directories.
Now let’s run tsndexer to generate the index.ts files automatically:
$ tsndexer ./src
(Here we run tsndexer pointing at the src directory. If you run it from the project root and your code is in src/, you could just run tsndexer with no arguments and it will default to the current directory.)
The tool will scan through src and each subfolder, creating index.ts (or index.tsx as needed) files. After running the command, our project structure might look like:
src
├── components
│ ├── Button.tsx
│ ├── Input.tsx
│ ├── common
│ │ ├── Icon.tsx
│ │ └── index.ts
│ └── index.ts
├── utils
│ ├── date.ts
│ ├── string.ts
│ └── index.ts
└── index.ts
Notice that tsndexer generated new index.ts files in each folder (components/index.ts, components/common/index.ts, utils/index.ts) and even a top-level src/index.ts. Each of these files contains exports for everything in that directory. Let’s inspect their contents:
src/components/index.ts:
export * from './Button';
export * from './Input';
export * from './common'; // exports everything from the common subfolder
Because components/common itself now has an index, exporting the subfolder as ./common effectively pulls in Icon as well.
src/components/common/index.ts:
export * from './Icon';
src/utils/index.ts:
export * from './date';
export * from './string';
src/index.ts:
export * from './components';
export * from './utils';
The root src/index.ts re-exports the modules in components and utils, which in turn cascade to all files inside them. This means the entire public surface of the src folder is now neatly bundled through these barrels.
With these in place, our import statements become much cleaner. For example, elsewhere in the project (or in another package that consumes this code), we can do:
// Importing directly from the barrels
import { Button, Input, Icon } from "src/components";
import { formatDate, parseDate } from "src/utils";
Or even, thanks to the root src/index.ts, import top-level:
import { Button, Icon, formatDate } from "src";
All the modules are available without deep paths. This greatly simplifies working with the project.
Dry Run and Verbose Output
If you’re curious or cautious about what tsndexer will do, you can use the --dry-run option. Running with --dry-run will simulate the index generation without actually writing any files. For example:
$ tsndexer --dry-run ./src
This might output something like:
[Dry Run] Would create:
- src/index.ts
- src/components/index.ts
- src/components/common/index.ts
- src/utils/index.ts
None of the files are written in dry-run mode; you just get a report of what it would do. This is useful to double-check which files will be affected, especially if you already have some index files and want to ensure the tool doesn’t overwrite something unexpectedly.
(By default, tsndexer will update existing index files if they exist. In practice it either regenerates them or appends missing exports – the exact behavior depends on the tool’s implementation. Generally, it’s safe to run it multiple times; it won’t duplicate exports.)
Advanced Options: Ignoring Files and Namespacing
Real projects often have files you don’t want to export (for instance, test files, storybook files, or internal utilities), or you may want to structure the exports differently. tsndexer provides options to handle these scenarios:
--ignore: Use this option to skip certain files or directories from indexing. You can provide a glob pattern or name to ignore. For example,--ignore "**/*.spec.ts"would telltsndexerto ignore any file ending in.spec.ts(perhaps unit tests) when generating barrels. You can ignore multiple patterns as well (e.g.--ignore "*/__tests__/*,*.stories.tsx"to ignore any files in a__tests__folder and any.stories.tsxfiles). This ensures that your barrel files only export the intended modules and omit those internal files. Another use-case: if you have certain files that must not be re-exported (maybe they’re for internal use only), you can exclude them via ignore rather than manually editing the index after generation.--namespace: This option is a special feature oftsndexerthat allows grouping exports under a namespace. By default, the tool uses flat exports (export * from '...';) which dumps all exported symbols into the containing index. With--namespace,tsndexerwill instead use theexport * as X from '...'syntax (ES2020 namespace exports) to group modules.
What does --namespace do? Essentially, instead of flattening all exports, it will treat each subdirectory as a grouped module. For example, without --namespace, our src/index.ts did this:
// default flat mode
export * from './components';
export * from './utils';
With --namespace, the root index might do:
// namespace mode
export * as components from './components';
export * as utils from './utils';
This means anyone importing from src could access things like components.Button or utils.formatDate. Likewise, inside components/index.ts, instead of exporting every Button and Input symbol directly, it might export the subfolder as a namespace:
export * as common from './common';
export * from './Button';
export * from './Input';
Now common is a namespace under components (so you’d use components.common.Icon to refer to the Icon component). Using namespaces in this way can be helpful if you want to preserve a logical grouping in your exports and avoid name collisions.
Comparing tsndexer to Other Barrel File Tools
The idea of auto-generating barrel files is not new – several tools exist in the TypeScript/JavaScript ecosystem. If you’ve been exploring this problem, you might have come across a few, such as barrelsby, create-ts-index (cti/ctix), Barrelbot, or editor plugins like “Index File Manager” for VSCode. Let’s compare these solutions to see how tsndexer stands out:
- barrelsby: A popular Node-based CLI for generating barrels. It’s typically installed via npm and run as part of your build or as a manual script. Barrelsby is quite configurable – you can choose which directories to index, what file extensions, whether to include default exports, and even different output structures (flat vs nested). In fact, barrelsby can achieve a nested structure but it does so by generating a more complex object assignment code, whereas
tsndexerleverages the modernexport * as namespacesyntax for simplicity. Barrelsby works well, but being an npm tool means adding it to your devDependencies and ensuring Node.js is available. By contrast,tsndexeris a single binary with no runtime dependencies – you can run it in environments without Node. Also, barrelsby’s rich options come at the cost of complexity;tsndexeraims to be straightforward for the common use-cases (with just a few flags as discussed). - Barrelbot: This is another approach – Barrelbot (an open-source project) can watch your filesystem and update index files on the fly as you add/remove files. It’s often run during development. While a watch mode is convenient, running a persistent process isn’t always necessary.
tsndexerdoesn’t currently have a watch mode (it’s a one-shot CLI), but you can always rerun it or script it to run on file changes (for example, integrate with a git hook or just run before builds). Barrelbot and similar tools again require Node – whereastsndexeris language-agnostic once compiled. - create-ts-index (cti/ctix): This is another CLI tool (Node-based, by Imjuni) that uses the TypeScript compiler API to generate index files. It’s quite powerful and can handle certain edge cases (like detecting duplicate exports or excluding specific exports via comments). The trade-off is complexity and speed; using the TS compiler API means it has a deeper understanding of your code, but also requires parsing your project’s AST which can be slower on huge codebases.
tsndexerlikely takes a simpler approach (scanning the filesystem and generating exports by file name). It may not catch duplicates or special cases with the same sophistication, but for most projects that’s not an issue. - VS Code Extensions / TS Plugins: Some developers use editor plugins like Index File Manager (VSCode) to generate or update barrel files when working in the IDE. These can be handy for individual productivity (e.g., you save a file and the extension updates the index file automatically). However, they are editor-dependent and not everyone on the team may have the same extension. There’s also a risk of forgetting to run it before committing. In contrast, a CLI like
tsndexercan be integrated into your build process or CI pipeline. For example, you might add a script"update-index": "tsndexer src"in yourpackage.jsonor Makefile, and even run it as part of a pre-commit hook or a CI step to ensure indexes are always up-to-date. This makes it a consistent part of your workflow, rather than a manual step some developers might skip.
In summary, many tools can solve the barrel problem, but `tsndexer’s advantages are its zero-dependency simplicity, speed, and the handy namespace grouping feature. You don’t need to install a bunch of packages or maintain a config; just run the binary. It’s also written in a compiled language (Go), which often means it can handle large file sets with great performance and low overhead.
Conclusion
Manually creating and updating index.ts files in a large TypeScript project is a recipe for frustration and bugs. It’s easy to forget an export or spend time fiddling with import paths – time that could be better spent building features. The tsndexer CLI tool steps in to eliminate this grunt work. It automatically scans your project structure and generates barrel files, ensuring that every module is exported and synchronized. This brings you the best of both worlds: you get the clean imports and organization that index files provide, without the maintenance burden and errors that come with doing it by hand.
To recap, using tsndexer can drastically improve your workflow by:
- Keeping all exports up-to-date (no missing or wrong exports causing runtime surprises).
- Simplifying your imports across the codebase (no more deep relative paths).
- Saving you from writing boilerplate index files ever again.
- Integrating easily into your development/CI process (thanks to being a lightweight CLI with no extra dependencies).
- Offering neat features like dry-run safety checks and namespace exports for advanced use cases.
If you’re a TypeScript developer dealing with dozens of files or managing a shared component library, give tsndexer a try. Automating your barrel files will reduce errors and make your project structure cleaner. As a result, you and your team can spend more time coding and less time housekeeping.
Happy coding, and may your imports stay tidy!